package com.atguigu.gmall.product.service.impl;

import com.atguigu.gmall.product.service.TestService;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author mgx
 * @version 1.0
 * @description: TODO
 * @date 2022/11/4 22:19
 */
@Service
@SuppressWarnings("all")
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 分布式锁的实现
     * 解决方案一：
     * *      使用的是redis的setnx -- “占坑思想实现”
     * * 操作步骤:
     * *      1. 获取数据前，先获取到锁 setnx lock 111
     * *      2. 获取到锁之后，在进行操作 ++num
     * *      3. 操作结束会后，释放锁 del key
     * 但是如果中间出现异常，会导致程序无法正常执行业务逻辑响应请求
     * 解决方案： 设置锁的超时释放时间
     * // 设置锁
     * Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
     * // 设置超时时间
     * stringRedisTemplate.expire("lock", 7, TimeUnit.SECONDS);
     * <p>
     * 但是上锁和设置释放时间不是原子性操作，如果异常出现在中间，也会导致获取到锁了但是还没设置超时时间一样是等待
     * 解决方案：
     * 使用redis的api直接在设置key的同时就设置key的超时时间
     * Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111", 7, TimeUnit.SECONDS);
     * <p>
     * 问题：怎么样设置合理的超时时间 -- 程序执行的过程慢，导致锁的早释放
     * <p>
     * 解决方案：
     * 1. 获取锁之前，先获取一个uuid, 作为key的唯一标识
     * 2. 将uuid作为value值存储
     * 3， 释放锁之前，取出value值与本地的uuid作比较，一致才删除
     * <p>
     * 增加标识uuid确实可以解决误删除的问题，但是在极端条件下还是会出现误删除的情况
     * <p>
     * 原因：判断和删除的操作非原子性
     * <p>
     * 解决方案：使用lua脚本保证它的执行是原子性的
     * <p>
     * Redisson的使用步骤：
     * 1. 将redissonClient对象注入到使用的类中
     * 2. 获取锁
     * 2.1 Lock lock = redission.getLock()
     * 2.2 lock.lock()
     * 3. 释放锁
     * lock.unlock()
     * <p>
     * 使用redisson可以不用自己实现自旋
     */

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public void testLock() {
        // 模拟sku查询
        String lockSku = "sku:" + 1314 + ":info";
        // 获取锁
        RLock lock = redissonClient.getLock(lockSku);
        // 设置超时时间
        // lock.lock(7, TimeUnit.SECONDS);
        try {
            // 设置超时时间和等待时间，第一个参数是等待时间，第二个参数是超时时间，当达到等到时间还是没有获取到锁就报错
            boolean flag = lock.tryLock(100, 10, TimeUnit.SECONDS);
            if (flag) { // 获取到锁
                // 加锁
                // lock.lock();
                String value = stringRedisTemplate.opsForValue().get("num");
                if (StringUtils.isEmpty(value)) {
                    return; // 如果是空值直接返回
                }

                // 不是的话就+1
                int num = Integer.parseInt(value);

                // 设置值进去
                stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
            } else {
                // 自己实现自旋
                Thread.sleep(50);
                testLock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    // 读锁测试
    @Override
    public String readLock() {
        // 初始化获取锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
        // 获取读锁
        RLock rLock = readWriteLock.readLock();
        // 加锁
        rLock.lock(10, TimeUnit.SECONDS);
        // 读取数据
        String msg = stringRedisTemplate.opsForValue().get("msg");
        // 释放锁
        rLock.unlock();
        return msg;
    }

    // 写锁测试
    @Override
    public String writeLock() {
        // 初始化获取锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
        // 获取写锁
        RLock rLock = readWriteLock.writeLock();
        // 加锁
        rLock.lock(10, TimeUnit.SECONDS);
        // 写数据到redis
        stringRedisTemplate.opsForValue().set("msg", String.valueOf(System.currentTimeMillis()));
        // 释放锁
        rLock.unlock();
        return "写入一条数据到redis中";
    }

    // @Override
    // public void testLock() {
    //     // 获取一个uuid
    //     String uuid = UUID.randomUUID().toString().replace("-", "");
    //     // 设置锁
    //     Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);
    //     // 异常
    //     // 设置超时时间
    //     // stringRedisTemplate.expire("lock", 7, TimeUnit.SECONDS);
    //     if(lock) { // 获取到锁
    //         String value = stringRedisTemplate.opsForValue().get("num");
    //         if(StringUtils.isEmpty(value)) {
    //             return; // 如果是空值直接返回
    //         }
    //
    //         // 不是的话就+1
    //         int num = Integer.parseInt(value);
    //
    //         // 设置值进去
    //         stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    //
    //         // lua脚本
    //         String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
    //                 "then\n" +
    //                 "    return redis.call(\"del\",KEYS[1])\n" +
    //                 "else\n" +
    //                 "    return 0\n" +
    //                 "end";
    //         // 创建redis脚本对象
    //         DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
    //         // 设置脚本
    //         defaultRedisScript.setScriptText(script);
    //         // 设置返回值类型
    //         defaultRedisScript.setResultType(Long.class);
    //
    //         // 执行删除脚本
    //         stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
    //
    //         // 获取value值判断
    //         /*if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))) {
    //             // 操作结束之后，释放锁
    //             stringRedisTemplate.delete("lock");
    //         }*/
    //     }else {
    //         // 没有获取到锁，自旋等待
    //         // 请求太快了，睡眠一下
    //         try {
    //             Thread.sleep(100);
    //             testLock();
    //         } catch (InterruptedException e) {
    //             e.printStackTrace();
    //         }
    //     }
    //
    // }

    public void testLockUUID() {
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);
        if (lock) {
            String num = stringRedisTemplate.opsForValue().get("num");
            int number = Integer.parseInt(num);
            stringRedisTemplate.opsForValue().set("num", String.valueOf(++number));
            String redisUUID = stringRedisTemplate.opsForValue().get("lock");
            if (uuid.equals(redisUUID)) {
                stringRedisTemplate.delete("lock");
            }
        } else {
            try {
                Thread.sleep(1000);
                testLockUUID();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    /**
     * 测试本地锁
     *
     * 使用压测工具进行高并发测试
     * ab  -n（一次发送的请求数）  -c（请求的并发数） 访问路径
     * ab -n 5000 -c 100 http://192.168.101.1:8206/admin/product/test/testLock
     *
     * 结果值本来应该是5000 结果是136
     * 加sychronized之后可以解决 -- 使用jvm自带的锁机制
     *
     * 当扩展成三台服务器8216， 8226之后，数据又不正确了
     * 解决思路：
     *      跨jvm的锁机制实现
     * 解决方案一：
     *      使用的是redis的setnx -- “占坑思想实现”
     * 操作步骤:
     *      1. 获取数据前，先获取到锁 setnx lock 111
     *      2. 获取到锁之后，在进行操作 ++num
     *      3. 操作结束会后，释放锁 del key
     */
    // @Override
    // public synchronized void testLock() {
    //     String value = stringRedisTemplate.opsForValue().get("num");
    //     if(StringUtils.isEmpty(value)) {
    //         return; // 如果是空值直接返回
    //     }
    //
    //     // 不是的话就+1
    //     int num = Integer.parseInt(value);
    //
    //     // 设置值进去
    //     stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    // }
}
